#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <unistd.h>

#ifdef USE_CAPSICUM
#include <sys/capsicum.h>
#endif

#include <err.h>
#include <sysexits.h>

#include "au.h"
#include "common.h"

#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef BSIZE
#define BSIZE 0x4000
#endif

enum {
	AU_SET_ENC = 0x01,
	AU_AVERAGE = 0x10,
	AU_NEEDHDR = 0x80
};

struct input {
	struct input	*prev;
	struct input	*next;
	struct au	 au;
	const char	*file;
	unsigned	 mult;
	int		 needhdr;
	int		 fd;
};

union value {
	uint64_t u;
	double d;
};
#ifdef SIGINFO
static size_t info_out = 0;
#endif

#ifdef SIGINFO
static void		 siginfo(int);
#endif
static int		 auopen(const char *);
static ssize_t		 in_gethdr(struct input *);
static ssize_t		 in_skipaux(struct input *, void *, size_t);
static size_t		 in_process(struct input *, void *, size_t,
   			    union value *, uint32_t *, int f);
static void		 au_value_put(struct au *, void *,
			    union value *, uint32_t *, size_t, int);
static struct input	*input_append(struct input **, const char *,
			    int);

int
main(int argc, char *argv[])
{
#ifdef USE_CAPSICUM
	cap_rights_t rights;
#endif
	struct au au = AU_INITIALIZER;
	struct input *input = NULL;
	struct input *tin;
	struct input *in;
	union value *value;
	uint32_t *count;
	FILE *fp;
	void *b;
	char *p;
	ssize_t r;
	size_t vcount;
	size_t bsize = BSIZE;
	size_t l;
	size_t i;
	unsigned long ul;
	int flags = AU_NEEDHDR;
	int c;

	while ((c = getopt(argc, argv, "C:ae:f:o:t")) != -1)
		switch (c) {
		case 'C':
			if ((fp = popen(optarg, "r")) == NULL)
				err(EX_NOINPUT,
				    "Could not popen command %s",
				    optarg);
			if (fcntl(fileno(fp), F_SETFD, FD_CLOEXEC) != 0)
				err(EX_OSERR,
				    "Could not set FD_CLOEXEC");
			(void)input_append(&input, optarg, fileno(fp));
			break;
		case 'a':
			flags |= AU_AVERAGE;
			break;
		case 'e':
			au.au_enc = strtou32(optarg);
			flags = AU_SET_ENC;
			break;
		case 'f':
			for (; *optarg != '\0'; optarg = p) {
				errno = 0;
				ul = strtoul(optarg, &p, 0);
				if (ul > INT_MAX)
					errno = ERANGE;
				if (errno != 0)
					err(EX_USAGE,
					    "Invalid number %s",
					    optarg);
				if (*p != '\0' && *p != ',')
					errx(EX_USAGE,
					    "Invalid number %s",
					    optarg);
				if (fcntl(ul, F_GETFL, &c) == -1)
					err(EX_USAGE, "Invalid "
					    "file-descriptor %lu", ul);
				(void)input_append(&input, NULL, ul);
			}
			break;
		case 'o':
			if ((c = auopen(optarg)) < 0)
				err(EX_NOINPUT,
				    "Could not auopen file %s", optarg);
			(void)input_append(&input, optarg, c);
			break;
		case 't':
			flags &= ~AU_AVERAGE;
			break;
		default:
			goto usage;
		}
	argc -= optind;
	argv += optind;
#ifdef USE_PLEDGE
	(void)pledge("stdio rpath", NULL);
#endif
	vcount = bsize / sizeof(double);
	if ((b = malloc(bsize)) == NULL ||
	    (count = malloc(vcount * sizeof(*count))) == NULL ||
	    (value = malloc(vcount * sizeof(*value))) == NULL)
		err(EX_OSERR, "Could not allocate buffers");
	for (; *argv != NULL; argv++) {
		if (strcmp(*argv, "-") == 0)
			(void)input_append(&input, NULL, STDIN_FILENO);
		else if ((c = open(*argv, O_RDONLY | O_CLOEXEC)) >= 0)
			(void)input_append(&input, *argv, c);
		else
			err(EX_NOINPUT, "Could not open file %s",
			    *argv);
	}
	if (input == NULL)
		(void)input_append(&input, NULL, STDIN_FILENO);
	for (in = input; in != NULL && in->fd != STDIN_FILENO;
	    in = in->next)
		/* do nothing */;
	if (in == NULL)
		(void)close(STDIN_FILENO);
#ifdef USE_PLEDGE
	(void)pledge("stdio", NULL);
#endif
#ifdef USE_CAPSICUM
	(void)cap_rights_init(&rights, CAP_READ);
	for (in = input; in != NULL && in->fd != STDIN_FILENO;
	    in = in->next)
		if (cap_rights_limit(in->fd, &rights) == -1 &&
		    errno != ENOSYS) {
			if (in->file != NULL)
				err(EX_OSERR, "Could not limit the "
				    "file %s", in->file);
			else
				err(EX_OSERR, "Could not limit the "
				    "file-descrptor %s", in->file);
		}
	(void)cap_rights_init(&rights, CAP_WRITE);
	if (cap_rights_limit(STDOUT_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard output");
	if (cap_rights_limit(STDERR_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard error");
#endif
#ifdef SIGINFO
	(void)signal(SIGINFO, &siginfo);
#endif
	while (input != NULL) {
		(void)memset(value, 0, vcount * sizeof(*value));
		(void)memset(count, 0, vcount * sizeof(*count));
		for (in = input; in != NULL; in = tin) {
			for (i = 0; i < vcount; ) {
				if (in->needhdr) {
					if ((r = in_gethdr(in)) <= 0)
						break;
					(void)in_skipaux(in, b, bsize);
				}
				if ((flags & AU_SET_ENC) != 0) {
					au.au_enc = in->au.au_enc;
					flags |= AU_SET_ENC;
				}
				au.au_rate = in->au.au_rate;
				au.au_chan = in->au.au_chan;
				if (in->au.au_size <= in->au.au_off) {
					in->needhdr = 1;
					continue;
				}
				l = MIN(bsize, in->au.au_size) /
				    au_vsize(&in->au);
				l = MIN(l, vcount - i) *
				    au_vsize(&in->au);
				if ((r = read(in->fd, b, l)) <= 0)
					break;
				i += in_process(in, b, r, value + i,
				    count + i, au.au_enc == AU_FLOAT32
				    || au.au_enc == AU_FLOAT64);
				if (in->au.au_size != UINT32_MAX)
					in->au.au_size -= r;
				in->needhdr = in->au.au_size <=
				    in->au.au_off;
			}
			tin = in->next;
			if (r < 0) {
				if (in->file != NULL)
					warn("Error reading audio for "
					    "file %s", in->file);
				else
					warn("Error reading audio for "
					    "file-descriptor %d",
					    in->fd);
				r = 0;
			}
			if (r == 0) {
				(void)close(in->fd);
				if (in->prev != NULL)
					in->prev->next = in->next;
				if (in->next != NULL)
					in->next->prev = in->prev;
				if (in == input)
					input = in->next;
				free(in);
			}
		}
		if (flags & AU_NEEDHDR) {
			(void)au_puthdr(&au, b);
			if (nwrite(STDOUT_FILENO, b, AU_SIZE)
			    != AU_SIZE)
				err(EX_IOERR,
				    "Could not write audio header");
			flags &= ~AU_NEEDHDR;
		}
		for (i = 0; i < vcount && count[i] > 0; i++)
			/* do nothing */;
		au_value_put(&au, b, value, count, i, flags);
		if (write(STDOUT_FILENO, b, i * au_vsize(&au)) < 0)
			err(EX_IOERR, "Write error");
#ifdef SIGINFO
		info_out += i;
#endif
	}
	return (0);
usage:
	(void)fprintf(stderr, "usage: aumix [-C command] [-d fds] "
	    "[-e encoding] [-o file] [file ...]\n");
	return (EX_USAGE);
}

#ifdef SIGINFO
void
siginfo(int sig)
{
	/* aumix: XXXXXX samples out */
	char b[7 + 6 + 14];
	size_t i = 0;

	(void)memcpy(b + i, "aumix: ", 8);
	i += 7;
	i += szwrite(b + i, info_out, NULL, NULL);
	(void)memcpy(b + i, " samples out\n", 4);
	i += 13;
	(void)write(STDERR_FILENO, b, i);
}
#endif

int
auopen(const char *f)
{
	pid_t pid;
	int p[2];

	if (pipe(p) != 0)
		err(EX_OSERR, "Could not open pipe for auopen(1)");
	if (fcntl(p[0], F_SETFD, FD_CLOEXEC) != 0)
		err(EX_OSERR, "Could not set FD_CLOEXEC for pipe");
	if ((pid = fork()) == -1)
		err(EX_OSERR, "Could not fork for auopen(1)");
	if (pid == 0) {
		if (dup2(p[1], STDOUT_FILENO) != STDOUT_FILENO)
			err(EX_OSERR, "Could not dup2 for auopen(1)");
		fprintf(stderr, "auopen %s\n", f);
		(void)execlp("auopen", "auopen", f, NULL);
		err(EX_USAGE, "Could not execute auopen(1)");
	}
	(void)close(p[1]);
	return(p[0]);
}

ssize_t
in_gethdr(struct input *in)
{
	char b[AU_SIZE];
	ssize_t r;

	if ((r = nread(in->fd, b, sizeof(b))) == -1) {
		if (in->file != NULL)
			warnx("Could not read audio header for file %s",
			    in->file);
		else
			warnx("Could not read audio header for "
			    "file-descrptor %d", in->fd);
	}
	if (r != sizeof(b))
		return (0);
	if (au_gethdr(&in->au, b) != 0)
		errx(EX_DATAERR, "Bad header");
	in->needhdr = 0;
	return (r);
}

ssize_t
in_skipaux(struct input *in, void *b, size_t s)
{
	ssize_t r;
	uint32_t i;

	for (i = AU_SIZE; i < in->au.au_off; i += r)
		if ((r = read(in->fd, b, MIN(s, in->au.au_off - i)))
		    <= 0)
			return (-1);
	return (i - AU_SIZE);
}

size_t
in_process(struct input *in, void *b, size_t r,
    union value *value, uint32_t *count, int f)
{
	const uint32_t end = 1;
	char fb[sizeof(double)];
	size_t s;
	size_t i;
	size_t j;
	size_t k;
	double d;
	int needd;
	uint32_t pcm;

	assert(in != NULL && b != NULL && value != NULL &&
	    count != NULL);
	for (i = 0, j = 0, s = au_vsize(&in->au); i + s <= r;
	    i += s, j++) {
		switch (in->au.au_enc) {
		case AU_PCM8:
			/* FALLTHROUGH */
		case AU_PCM16:
			/* FALLTHROUGH */
		case AU_PCM24:
			/* FALLTHROUGH */
		case AU_PCM32:
			for (k = 0, pcm = 0; k < s; k++)
				pcm = (pcm << 8) |
				    ((uint8_t *)b)[i + k];
			switch (s) {
			case 1:
				pcm |= pcm << 8;
				/* FALLTHOURGH */
			case 2:
				pcm |= pcm << 16;
				break;
			case 3:
				pcm = (pcm << 8) | (pcm >> 16);
			}
			needd = 1;
			break;
		case AU_FLOAT32:
			if (*(char *)&end)
				for (k = 0; k < s; k++)
					fb[s - k - 1] =
					    ((char *)b)[i + k];
			else
				(void)memcpy(fb, (char *)b + i, s);
			d = *(float *)fb;
			needd = 0;
			break;
		case AU_FLOAT64:
			if (*(char *)&end)
				for (k = 0; k < s; k++)
					fb[s - k - 1] =
					    ((char *)b)[i + k];
			else
				(void)memcpy(fb, (char *)b + i, s);
			d = *(double *)fb;
			needd = 0;
			break;
		default:
			errx(EX_DATAERR, "Unsupported encoding %s",
			    au_encstr(in->au.au_enc));
		}
		if (f) {
			if (needd)
				d = (pcm >> 31) ? (double)(~pcm + 1) /
				    (double)PCM32_MIN : (double)pcm /
				    (double)PCM32_MAX;
			value[j].d += d * (double)in->mult;
		} else {
			if (!needd)
				pcm = d < 0 ?
				    -d * (double)PCM32_MIN :
				    d * (double)PCM32_MAX;
			value[j].u += ((uint64_t)pcm ^
			    (UINT64_C(1) << 31)) - (UINT64_C(1) << 31);
		}
		count[j]++;
	}
	return (j);
}

void
au_value_put(struct au *au, void *b, union value *value,
    uint32_t *count, size_t vcount, int flags)
{
	uint8_t *up = b;
	float *fp = b;
	double *dp = b;
	size_t s;
	size_t i;
	size_t j;
	uint32_t pcm;

	assert(au != NULL && b != NULL && value != NULL &&
	    count != NULL);
	s = au_vsize(au);
	if (flags & AU_AVERAGE)
		goto average;
	/* truncate */
	switch (au->au_enc) {
	case AU_PCM8:
		/* FALLTHROUGH */
	case AU_PCM16:
		/* FALLTHROUGH */
	case AU_PCM24:
		/* FALLTHROUGH */
	case AU_PCM32:
		for (up = b, i = 0; i < vcount; i++) {
			pcm = (value[i].u >> 63) ?
			    (~value[i].u + 1 > PCM32_MIN ?
			    PCM32_MIN : value[i].u) :
			    MIN(value[i].u, PCM32_MAX);
			for (j = 0; j < s; j++)
				*up++ = (pcm >> (4 - j - 1) * 8) & 0xFF;
		}
		break;
	case AU_FLOAT32:
		for (fp = b, i = 0; i < vcount; i++)
			fp[i] = value[i].d < -1 ? -1 :
			    (value[i].d > 1 ? 1 : value[i].d);
		au_nswap(au, b, s * vcount);
		break;
	case AU_FLOAT64:
		for (dp = b, i = 0; i < vcount; i++)
			dp[i] = value[i].d < -1 ? -1 :
			    (value[i].d > 1 ? 1 : value[i].d);
		au_nswap(au, b, s * vcount);
		break;
	}
	return;
average:
	switch (au->au_enc) {
	case AU_PCM8:
		/* FALLTHROUGH */
	case AU_PCM16:
		/* FALLTHROUGH */
	case AU_PCM24:
		/* FALLTHROUGH */
	case AU_PCM32:
		for (up = b, i = 0; i < vcount; i++) {
			pcm = value[i].u / count[i];
			for (j = 0; j < s; j++)
				*up++ = (pcm >> (4 - j - 1) * 8) & 0xFF;
		}
		break;
	case AU_FLOAT32:
		for (fp = b, i = 0; i < vcount; i++)
			fp[i] = value[i].d / (double)count[i];
		au_nswap(au, b, s * vcount);
		break;
	case AU_FLOAT64:
		for (dp = b, i = 0; i < vcount; i++)
			dp[i] = value[i].d / (double)count[i];
		au_nswap(au, b, s * vcount);
		break;
	}
}

struct input *
input_append(struct input **i, const char *f, int fd)
{
	struct input *p = NULL;
	struct input *n;

	assert(i != NULL && fd >= 0);
	for (n = *i; n != NULL && n->fd != fd; p = n, n = n->next)
		if (n->fd == fd) {
			n->mult++;
			return (n);
		}
	if ((n = malloc(sizeof(struct input))) == NULL)
		err(EX_OSERR, "Could not allocate input");
	n->prev = p;
	n->next = NULL;
	n->file = f;
	n->mult = 1;
	n->needhdr = 1;
	n->fd = fd;
	if (p != NULL)
		p->next = n;
	if (*i == NULL)
		*i = n;
	return (n);
}
